﻿using Microsoft.Win32;
using Microsoft.Win32.SafeHandles;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

namespace SparkBench
{
    public class SerialPorts
    {
        private static Guid GUID_DEVINTERFACE_COMPORT = new Guid("{86E0D1E0-8089-11D0-9CE4-08003E301F73}");
        private static Guid GUID_DEVINTERFACE_SERENUM_BUS_ENUMERATOR = new Guid("{4D36E978-E325-11CE-BFC1-08002BE10318}");

        private const UInt32 DICS_FLAG_GLOBAL = 0x00000001;
        private const UInt32 DIREG_DEV = 0x00000001;
        private const UInt32 KEY_QUERY_VALUE = 0x0001;

        // Reference for Win32 error codes:
        // http://msdn.microsoft.com/en-us/library/cc231199.aspx
        private const int ERROR_NO_MORE_ITEMS = 0x00000103;

        public static List<Tuple<string, string>> ListPorts()
        {
            List<Tuple<string, string>> retVal = new List<Tuple<string, string>>();

            IntPtr comPortsDeviceList = NativeMethods.SetupDiGetClassDevs(ref GUID_DEVINTERFACE_SERENUM_BUS_ENUMERATOR, null, IntPtr.Zero, (uint)(DiGetClassFlags.DIGCF_PRESENT | DiGetClassFlags.DIGCF_DEVICEINTERFACE));

            try
            {
                SP_DEVINFO_DATA deviceData = new SP_DEVINFO_DATA();
                deviceData.cbSize = (uint) Marshal.SizeOf(deviceData);

                uint deviceIndex = 0;
                bool success;

                do
                {
                    success = NativeMethods.SetupDiEnumDeviceInfo(comPortsDeviceList, deviceIndex, ref deviceData);

                    if (!success)
                    {
                        int lastError = Marshal.GetLastWin32Error();

                        NativeMethods.SetupDiDestroyDeviceInfoList(comPortsDeviceList);

                        if (lastError == ERROR_NO_MORE_ITEMS)
                            return retVal;
                        else
                            throw new Win32Exception(lastError);
                    }

                    UInt32 RegType;
                    UInt32 RequiredSize;
                    byte[] ptrBuf = null;

                    // Call SetupDiGetDeviceRegistryProperty with null buffer in order to get size of required buffer
                    if (NativeMethods.SetupDiGetDeviceRegistryProperty(comPortsDeviceList, ref deviceData, (uint)SetupDiGetDeviceRegistryPropertyEnum.SPDRP_DEVICEDESC, out RegType, null, 0, out RequiredSize))
                    {
                        ptrBuf = new byte[RequiredSize];

                        // Now call SetupDiGetDeviceRegistryProperty with a buffer of the correct size allocated
                        if (NativeMethods.SetupDiGetDeviceRegistryProperty(comPortsDeviceList, ref deviceData, (uint)SetupDiGetDeviceRegistryPropertyEnum.SPDRP_DEVICEDESC, out RegType, ptrBuf, 255, out RequiredSize))
                        {
                            string deviceDescription = Encoding.Unicode.GetString(ptrBuf);

                            // This is the same way the Spark CLI uses to find Spark COM ports 
                            if (deviceDescription.StartsWith("Spark"))
                            {
                                // Registry settings for Serial Devices:
                                // http://msdn.microsoft.com/en-us/library/windows/hardware/ff546972%28v=vs.85%29.aspx
                                object portName = ReadDeviceRegistryKey(comPortsDeviceList, deviceData, "PortName");

                                retVal.Add(new Tuple<string, string>((string)portName, deviceDescription));
                            }
                        }
                    }

                    //NativeMethods.SetupDiGetDeviceRegistryProperty(comPortsDeviceList, ref deviceData, (uint)SetupDiGetDeviceRegistryPropertyEnum.SPDRP_FRIENDLYNAME, out RegType, null, 0, out RequiredSize);
                    //ptrBuf = new byte[RequiredSize];

                    //if (NativeMethods.SetupDiGetDeviceRegistryProperty(comPortsDeviceList, ref deviceData, (uint)SetupDiGetDeviceRegistryPropertyEnum.SPDRP_FRIENDLYNAME, out RegType, ptrBuf, 255, out RequiredSize))
                    //{
                    //    string ControllerDeviceDesc = Encoding.Unicode.GetString(ptrBuf);
                    //}

                    deviceIndex++;
                }
                while (success);
            }
            finally
            {
                NativeMethods.SetupDiDestroyDeviceInfoList(comPortsDeviceList);
            }

            return retVal;
        }

        // Original reference for figuring out the need to read from the registry:
        // http://www.nakov.com/blog/2009/05/10/enumerate-all-com-ports-and-find-their-name-and-description-in-c/
        private static object ReadDeviceRegistryKey(IntPtr pDevInfoSet, SP_DEVINFO_DATA deviceInfoData, string keyName)
        {
            IntPtr hDeviceRegistryKey = NativeMethods.SetupDiOpenDevRegKey(pDevInfoSet, ref deviceInfoData, DICS_FLAG_GLOBAL, 0, DIREG_DEV, KEY_QUERY_VALUE);

            if (hDeviceRegistryKey == IntPtr.Zero)
                throw new Win32Exception();

            SafeRegistryHandle registryHandle = new SafeRegistryHandle(hDeviceRegistryKey, true);
            RegistryKey deviceKey = RegistryKey.FromHandle(registryHandle);

            return deviceKey.GetValue(keyName);
        }
    }

    [StructLayout(LayoutKind.Sequential)]
    struct SP_DEVINFO_DATA
    {
        public uint cbSize;
        public Guid classGuid;
        public uint devInst;
        public IntPtr reserved;
    }

    // Reference documentation for SetupAPI functions:
    // http://msdn.microsoft.com/en-us/library/windows/hardware/ff551967%28v=vs.85%29.aspx
    class NativeMethods
    {
        [DllImport("setupapi.dll", SetLastError = true)]
        public extern static IntPtr SetupDiCreateDeviceInfoList(ref Guid ClassGuid, IntPtr hwndParent);

        [DllImport("setupapi.dll", SetLastError = true)]
        public static extern bool SetupDiEnumDeviceInfo(IntPtr DeviceInfoSet, uint MemberIndex, ref SP_DEVINFO_DATA DeviceInfoData);

        [DllImport("setupapi.dll", CharSet = CharSet.Auto)]
        public static extern IntPtr SetupDiGetClassDevs(ref Guid ClassGuid,
            [MarshalAs(UnmanagedType.LPTStr)] string Enumerator,
            IntPtr hwndParent,
            uint Flags
            );

        [DllImport("setupapi.dll", CharSet = CharSet.Auto, SetLastError = true)]
        public static extern bool SetupDiGetDeviceRegistryProperty(
            IntPtr deviceInfoSet,
            ref SP_DEVINFO_DATA deviceInfoData,
            uint property,
            out UInt32 propertyRegDataType,
            [Out] byte[] propertyBuffer,
            uint propertyBufferSize,
            out UInt32 requiredSize
            );

        [DllImport("setupapi.dll", SetLastError = true)]
        public static extern bool SetupDiDestroyDeviceInfoList(IntPtr DeviceInfoSet);

        [DllImport("Setupapi", CharSet = CharSet.Auto, SetLastError = true)]
        public static extern IntPtr SetupDiOpenDevRegKey(IntPtr hDeviceInfoSet, ref SP_DEVINFO_DATA deviceInfoData, uint scope,
            uint hwProfile, uint parameterRegistryValueKind, uint samDesired);
    }

    [Flags]
    enum DiGetClassFlags : uint
    {
        DIGCF_DEFAULT = 0x00000001,  // only valid with DIGCF_DEVICEINTERFACE
        DIGCF_PRESENT = 0x00000002,
        DIGCF_ALLCLASSES = 0x00000004,
        DIGCF_PROFILE = 0x00000008,
        DIGCF_DEVICEINTERFACE = 0x00000010,
    }

    /// <summary>
    /// Flags for SetupDiGetDeviceRegistryProperty().
    /// </summary>
    enum SetupDiGetDeviceRegistryPropertyEnum : uint
    {
        SPDRP_DEVICEDESC = 0x00000000, // DeviceDesc (R/W)
        SPDRP_HARDWAREID = 0x00000001, // HardwareID (R/W)
        SPDRP_COMPATIBLEIDS = 0x00000002, // CompatibleIDs (R/W)
        SPDRP_UNUSED0 = 0x00000003, // unused
        SPDRP_SERVICE = 0x00000004, // Service (R/W)
        SPDRP_UNUSED1 = 0x00000005, // unused
        SPDRP_UNUSED2 = 0x00000006, // unused
        SPDRP_CLASS = 0x00000007, // Class (R--tied to ClassGUID)
        SPDRP_CLASSGUID = 0x00000008, // ClassGUID (R/W)
        SPDRP_DRIVER = 0x00000009, // Driver (R/W)
        SPDRP_CONFIGFLAGS = 0x0000000A, // ConfigFlags (R/W)
        SPDRP_MFG = 0x0000000B, // Mfg (R/W)
        SPDRP_FRIENDLYNAME = 0x0000000C, // FriendlyName (R/W)
        SPDRP_LOCATION_INFORMATION = 0x0000000D, // LocationInformation (R/W)
        SPDRP_PHYSICAL_DEVICE_OBJECT_NAME = 0x0000000E, // PhysicalDeviceObjectName (R)
        SPDRP_CAPABILITIES = 0x0000000F, // Capabilities (R)
        SPDRP_UI_NUMBER = 0x00000010, // UiNumber (R)
        SPDRP_UPPERFILTERS = 0x00000011, // UpperFilters (R/W)
        SPDRP_LOWERFILTERS = 0x00000012, // LowerFilters (R/W)
        SPDRP_BUSTYPEGUID = 0x00000013, // BusTypeGUID (R)
        SPDRP_LEGACYBUSTYPE = 0x00000014, // LegacyBusType (R)
        SPDRP_BUSNUMBER = 0x00000015, // BusNumber (R)
        SPDRP_ENUMERATOR_NAME = 0x00000016, // Enumerator Name (R)
        SPDRP_SECURITY = 0x00000017, // Security (R/W, binary form)
        SPDRP_SECURITY_SDS = 0x00000018, // Security (W, SDS form)
        SPDRP_DEVTYPE = 0x00000019, // Device Type (R/W)
        SPDRP_EXCLUSIVE = 0x0000001A, // Device is exclusive-access (R/W)
        SPDRP_CHARACTERISTICS = 0x0000001B, // Device Characteristics (R/W)
        SPDRP_ADDRESS = 0x0000001C, // Device Address (R)
        SPDRP_UI_NUMBER_DESC_FORMAT = 0X0000001D, // UiNumberDescFormat (R/W)
        SPDRP_DEVICE_POWER_DATA = 0x0000001E, // Device Power Data (R)
        SPDRP_REMOVAL_POLICY = 0x0000001F, // Removal Policy (R)
        SPDRP_REMOVAL_POLICY_HW_DEFAULT = 0x00000020, // Hardware Removal Policy (R)
        SPDRP_REMOVAL_POLICY_OVERRIDE = 0x00000021, // Removal Policy Override (RW)
        SPDRP_INSTALL_STATE = 0x00000022, // Device Install State (R)
        SPDRP_LOCATION_PATHS = 0x00000023, // Device Location Paths (R)
        SPDRP_BASE_CONTAINERID = 0x00000024  // Base ContainerID (R)
    }
}
